package com.magic0610.MagicYaml;

import java.io.File;
import java.io.IOException;
import java.io.InputStream;
import java.io.InputStreamReader;
import java.nio.file.Files;
import java.util.ArrayList;
import java.util.Collection;
import java.util.HashSet;
import java.util.Set;

import com.magic0610.MagicYaml.BukkitConfiguration.file.YamlConfiguration;
import com.magic0610.MagicYaml.Exception.BackupFileAlreadyExist;
import com.magic0610.MagicYaml.Exception.InternalLoadFailedException;
import com.magic0610.MagicYaml.Exception.MagicIOException;

public class YamlConfig {
    private final File file;
    private YamlConfiguration config;
    private org.bukkit.configuration.file.YamlConfiguration bukkitConfig;
    private final String internalPath;
    private int version;
    private int internalVersion;
    private final boolean bukkitSupport;
    private Collection<String> whiteList;

    /*
     * 包私有构造函数，禁止外部使用
     * 抛出异常: InternalLoadFailedException
     */
    YamlConfig(File file, String internalBasePath, String internalPath, boolean bukkitSupport) {
        this.file = file;
        this.bukkitSupport = bukkitSupport;
        whiteList = new ArrayList<>();
        this.internalPath = (internalBasePath == null ? internalPath : internalBasePath + internalPath);
        //internalBasePath不为null则把两个路径组合得到最终的路径
    }

    /*
     * 加载配置文件
     */
    void loadConfig() {
        if (bukkitSupport) { //若开启Bukkit支持则使用原生对象加载文件
            bukkitConfig = org.bukkit.configuration.file.YamlConfiguration.loadConfiguration(file);
            config = null;
        } else { //否则使用经过阉割的自带对象
            config = YamlConfiguration.loadConfiguration(file); //加载配置文件
            bukkitConfig = null;
        }
    }

    /*
     * 更新版本号
     * 抛出异常: MagicIOException
     */
    void updateVersion() {
        if (bukkitSupport) { //外部配置文件
            if (bukkitConfig.contains("version")) { //版本号存在则获取
                version = bukkitConfig.getInt("version");
            }
        } else {
            if (config.contains("version")) { //版本号存在则获取
                version = config.getInt("version");
            }
        }

        try(InputStream stream = MagicYaml.class.getResourceAsStream(internalPath)) { //内部配置文件
            if (stream == null) return; //读取不到文件时返回
            YamlConfiguration internalConfig = YamlConfiguration.loadConfiguration(new InputStreamReader(stream)); //读取配置文件
            if (internalConfig.contains("version")) { //内部版本号存在则获取
                internalVersion = internalConfig.getInt("version"); //内部版本号读取
            }
        } catch (IOException exception) {
            throw new MagicIOException(exception);
        }
    }

    /**
     * <p>获取文件</p>
     * 
     * @return File - 配置文件
     */
    public File getFile() {
        return file;
    }

    /**
     * <p>获取配置文件</p>
     * <p>此函数获取的是由MagicYaml修改后的配置文件对象，对比Bukkit的原生对象有所阉割</p>
     * <p>如果你正在开发Bukkit插件，你应该启用Bukkit支持并使用getBukkitConfig()</p>
     * <p>若Bukkit支持已开启，但你执意使用此函数，那么你将会得到null</p>
     * 
     * @return YamlConfiguration(MagicYaml) - 配置文件
     */
    public YamlConfiguration getConfig() {
        return config;
    }

    /**
     * <p>获取配置文件</p>
     * <p>此函数获取的是Bukkit的原生配置文件对象</p>
     * <p>如果你并非正在开发Bukkit插件，你应该关闭Bukkit支持并使用getConfig()</p>
     * <p>若Bukkit支持已关闭，但你执意使用此函数，那么你将会得到null</p>
     * 
     * @return YamlConfiguration(Bukkit) - 配置文件
     */
    public org.bukkit.configuration.file.YamlConfiguration getBukkitConfig() {
        return bukkitConfig;
    }

    /**
     * <p>获取配置文件Jar内部路径</p>
     * 
     * @return String - 配置文件内部路径
     */
    public String getInternalPath() {
        return internalPath;
    }

    /**
     * <p>将配置文件保存到文件中</p>
     * <p>关闭Bukkit支持时使用此方法</p>
     * 
     * @param config - 配置文件(YamlConfiguration)(MagicYaml)
     * 
     * @throws MagicIOException 保存配置时遇到IO错误
     */
    public void save(YamlConfiguration config) {
        this.config = config;
        try {
            config.save(file);
        } catch (IOException exception) {
            throw new MagicIOException(exception);
        }
    }

    /**
     * <p>将配置文件保存到文件中</p>
     * <p>开启Bukkit支持时使用此方法</p>
     * 
     * @param bukkitConfig - 配置文件(YamlConfiguration)(Bukkit)
     * 
     * @throws MagicIOException 保存配置时遇到IO错误
     */
    public void save(org.bukkit.configuration.file.YamlConfiguration bukkitConfig) {
        this.bukkitConfig = bukkitConfig;
        try {
            bukkitConfig.save(file);
        } catch (IOException exception) {
            throw new MagicIOException(exception);
        }
    }

    /**
     * <p>检查配置文件是否需要更新</p>
     * <p>只判断内部和外部配置文件的版本是否不同, 两版本号大小不影响判断结果</p>
     * 
     * @return boolean - 是否需要更新
     */
    public boolean needUpdate() {
        return version != internalVersion;
    }

    /**
     * <p>将数据存储目录的配置文件更新</p>
     * <p>旧文件中存在新文件没有的键，此键将会被删除</p>
     * <p>备份文件被命名为“BK-时间戳-源文件名”</p>
     * <p>此更新函数耗时较长，故使用此函数前最好使用needUpdate()判断是否需要更新</p>
     * <p>更新后请使用getConfig()或getBukkitConfig()再次获取配置文件</p>
     * 
     * @param backup - 是否备份原始文件(boolean)
     * 
     * @return File - 备份文件(关闭备份时返回null)
     * 
     * @throws BackupFileAlreadyExist 试图创建备份时文件夹下已有同名文件
     * @throws MagicIOException 备份文件时复制失败或保存时出错
     * @throws InternalLoadFailedException 读取Jar内部配置文件出错
     */
    public File update(boolean backup) {
        File backupFile = null;
        if (backup) { //创建备份文件
            backupFile = new File(file.getParentFile(), "BK-" + System.currentTimeMillis() + "-" + file.getName());
            if (backupFile.exists()) { //文件已存在则抛异常
                throw new BackupFileAlreadyExist(backupFile.getName());
            }
            try { //复制文件
                Files.copy(file.toPath(), backupFile.toPath());
            } catch (IOException exception) {
                throw new MagicIOException(exception);
            }
        }

        Functions.saveInternalFile(this, true); //用Jar内部配置文件覆盖旧文件

        if (bukkitSupport) { //使用Bukkit方法
            updateBukkitConfig();
        } else { //使用自带方法
            updateInternalConfig();
        }
        updateVersion(); //更新版本号
        return backupFile;
    }

    /* 去除每个子健的父键，获取要更新的键 */
    private Set<String> filterKeys(Set<String> keys) {
        String lastKey = null;
        String deleting = null;
        Set<String> removeKeys = new HashSet<>();
        for (String key : keys) {
            if (whiteList.contains(key)) {
                deleting = key;
            } //删除所有 父键在白名单内 的键
            if (deleting != null && key.startsWith(deleting)) {
                removeKeys.add(key);
            }

            if (lastKey != null && key.startsWith(lastKey + ".")) {
                removeKeys.add(lastKey);
            } //删除父键逻辑
            lastKey = key;
        } //遍历每一个键，若下一个键以上一个键开头，则移除上一个键
        removeKeys.add("version");
        keys.removeAll(removeKeys); //此处删除键是不保留用户配置的键
        keys.addAll(whiteList); //将所有白名单加入以保存用户自定义数据
        return keys;
    }

    /* 更新Bukkit配置文件 */
    private void updateBukkitConfig() {
        org.bukkit.configuration.file.YamlConfiguration newBukkitConfig = 
            org.bukkit.configuration.file.YamlConfiguration.loadConfiguration(file);
        Set<String> keys = filterKeys(newBukkitConfig.getKeys(true));
        for (String key : keys) { //若能从旧配置文件获取到值则写入新配置文件
            Object object = bukkitConfig.get(key);
            if (object != null) {
                newBukkitConfig.set(key, object);
            }
        }
        save(newBukkitConfig);
    }

    /* 更新自带配置文件 */
    private void updateInternalConfig() {
        YamlConfiguration newConfig = YamlConfiguration.loadConfiguration(file);
        Set<String> keys = filterKeys(newConfig.getKeys(true));
        for (String key : keys) { //若能从旧配置文件获取到值则写入新配置文件
            Object object = config.get(key);
            if (object != null) {
                newConfig.set(key, object);
            }
        }
        save(newConfig);
    }

    /**
     * <p>增加白名单项目</p>
     * <p>当某个键在白名单下时，这个键下的所有内容将不会被更新</p>
     * <p>适用于在配置文件中的键值对，防止用户自定义的键在更新时被更改</p>
     * <p>若某个在白名单中的键在新的配置文件中已经不存在，它也一样会被删除</p>
     * 
     * @param key - 要加入白名单的键(String)
     */
    public void addWhiteList(String key) {
        whiteList.add(key);
    }

    /**
     * <p>增加白名单项目</p>
     * <p>当某个键在白名单下时，这个键下的所有内容将不会被更新</p>
     * <p>适用于在配置文件中的键值对，防止用户自定义的键在更新时被更改</p>
     * <p>若某个在白名单中的键在新的配置文件中已经不存在，它也一样会被删除</p>
     * 
     * @param keys - 要加入白名单的键集合{@code (Collection<String>)}
     */
    public void addWhiteList(Collection<String> keys) {
        whiteList.addAll(keys);
    }

    /**
     * <p>删除白名单项目</p>
     * 
     * @param key - 要在白名单中删除的键(String)
     */
    public void removeWhiteList(String key) {
        whiteList.remove(key);
    }

    /**
     * <p>删除白名单项目</p>
     * 
     * @param keys - 要在白名单中删除的键集合{@code (Collection<String>)}
     */
    public void removeWhiteList(Collection<String> keys) {
        whiteList.removeAll(keys);
    }

    /**
     * <p>设置白名单集合，此操作将覆盖旧的白名单</p>
     * <p>当某个键在白名单下时，这个键下的所有内容将不会被更新</p>
     * <p>适用于在配置文件中的键值对，防止用户自定义的键在更新时被更改</p>
     * <p>若某个在白名单中的键在新的配置文件中已经不存在，它也一样会被删除</p>
     * 
     * @param whiteList - 要设置的白名单集合{@code (Collection<String>)}
     */
    public void setWhiteList(Collection<String> whiteList) {
        this.whiteList = whiteList;
    }

    /**
     * <p>获取白名单集合</p>
     * 
     * @return {@code Collection<String>} - 白名单集合
     */
    public Collection<String> getWhiteList() {
        return whiteList;
    }
}
